Skip to content

[wrangler] Recognise compound statement markers without surrounding whitespace - #15226

Open
MatheusMartinho wants to merge 6 commits into
cloudflare:mainfrom
MatheusMartinho:fix/d1-splitter-compound-delimiters
Open

[wrangler] Recognise compound statement markers without surrounding whitespace#15226
MatheusMartinho wants to merge 6 commits into
cloudflare:mainfrom
MatheusMartinho:fix/d1-splitter-compound-delimiters

Conversation

@MatheusMartinho

@MatheusMartinho MatheusMartinho commented Aug 16, 2026

Copy link
Copy Markdown

Fixes #15228. Related to #15093 (see note below).

splitSqlQuery() splits a SQL file into statements for wrangler d1 execute --file and wrangler d1 migrations apply. It tracks compound statements so that semicolons inside a trigger body do not split it, but it only recognised the BEGIN/CASE/END markers when they were surrounded by whitespace:

/\s(BEGIN|CASE)\s$/i
/\sEND[;\s]$/i

SQLite also accepts those markers delimited by punctuation, and then the split goes wrong in two ways:

  • INSERT INTO audit VALUES (new.id);END; — the END is preceded by ;, so the compound statement never terminates and every following statement is swallowed into the trigger and sent to D1 as one statement. The visible symptom is silently missing result sets, not an error.
  • CREATE TRIGGER ... WHEN (1=1)BEGIN — the BEGIN is preceded by ), so the compound statement never starts and the trigger body is split at its first internal semicolon.

Both markers are now matched when delimited by any non-identifier character, using a negative lookbehind so identifiers that merely end in a keyword — a weekend table, a value of 'weekend' — still do not match.

Review follow-up: bracket-quoted and non-ASCII identifiers

The Devin review correctly caught that widening the delimiter class introduced two false positives, which are now fixed:

  • Bracket-quoted identifiers: [ and ] are outside the identifier class, so a trigger body containing a name like [end] popped the compound statement early and the trigger was chopped apart. splitSqlIntoStatements() now consumes [] like the other quote styles. This also fixes [wrangler] D1 SQL splitter does not treat bracket-quoted identifiers as quoted #15228, which is pre-existing: a ; inside a bracket-quoted identifier (CREATE TABLE metrics ([value;unit] TEXT);) splits the statement even on mainnormalizeSqlLineEndings() in the same file already treats [] as a quote pair, so the two scanners disagreed about the same syntax. (My earlier review reply claiming the [end] case reproduced identically on main was wrong — on main the whitespace-only markers coincidentally never fire inside [end]; I've corrected that in the thread.)
  • Identifiers with diacritics: the ASCII-only class treated a letter such as é as a delimiter, so a néend column matched the END marker. Both regexes now use Unicode identifier classes (\p{L}\p{N}_$ with the u flag), consistent with how isDollarQuoteIdentifier() in the same file treats letters with diacritical marks.

On #15093

That issue reports the same class of bug for a lowercase end;. Case handling is already fixed on main (both regexes carry /i), and I confirmed lowercase input now splits identically to uppercase, so the originally reported reproduction passes as-is. The variants above still reproduced, which is what this PR fixes.

Testing

Seven regression tests in splitter.test.ts, red/green verified:

  • ;END;, (1=1)BEGIN and SUM(CASE ... END) fail against main and pass with this change; the false-positive guard (weekend) passes both before and after.
  • [value;unit] fails on main and on this branch before the scanner change (it is the [wrangler] D1 SQL splitter does not treat bracket-quoted identifiers as quoted #15228 bug), and passes now.
  • The trigger-body [end] and néend cases pass on main, failed on this branch before the follow-up commits (the regressions Devin found), and pass again now.

Full file passing; check:type and check:format are clean. I also verified end-to-end with wrangler d1 execute --local --file that the [value;unit] schema fails on released wrangler 4.123.0 and that the trigger examples execute correctly with this branch.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this fixes splitting of SQL that the documented behaviour already implies is supported; no user-facing interface changes.

Note

This contribution was written with an AI agent: Claude Code (Claude Fable 5), directed and reviewed by @MatheusMartinho, who takes responsibility for the change.


Devin Review

…hitespace

The D1 SQL splitter only treated BEGIN, CASE and END as compound
statement markers when they were surrounded by whitespace. SQLite also
accepts them delimited by punctuation, so a trigger body ending in
`INSERT ...;END;` never terminated and every following statement was
swallowed into it and sent as one statement, and a trigger declared with
`WHEN (1=1)BEGIN` was split in the middle of its body.

Match the markers when they are delimited by any non-identifier
character, keeping identifiers that merely end in a keyword, such as a
`weekend` table, from matching.
@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 975aa23

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/d1
  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/olive-donuts-battle.md: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/d1/splitter.test.ts: [@cloudflare/d1 @cloudflare/wrangler]
  • packages/wrangler/src/d1/splitter.ts: [@cloudflare/d1 @cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Aug 16, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15226

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15226

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15226

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15226

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15226

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15226

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15226

miniflare

npm i https://pkg.pr.new/miniflare@15226

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15226

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15226

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15226

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15226

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15226

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15226

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15226

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15226

wrangler

npm i https://pkg.pr.new/wrangler@15226

commit: 975aa23

…racter

Relaxing only the character before the marker made a parenthesised CASE
expression, such as SUM(CASE WHEN a THEN 1 ELSE 0 END), open a compound
statement that END) never closed, so the rest of the file was sent as a
single statement. Accept any non-identifier character after END too, and
cover the case in the tests.
devin-ai-integration[bot]

This comment was marked as resolved.

@MatheusMartinho

MatheusMartinho commented Aug 16, 2026

Copy link
Copy Markdown
Author

The three red Tests jobs are unrelated to this change — they are container, dev-registry and browser-rendering fixtures:

Job Failing task Symptom
Tests (Linux, fixtures) @fixture/interactive-dev container image never comes up: runc run failed: container process is already dead, then Container is not listening to port 8080
Tests (macOS, fixtures) @fixture/dev-registry supports fetch over service binding times out after 58s (already retried)
Tests (Windows, packages-and-tools) miniflare 18 of 21 plugins/browser tests fail, plus one dev-registry timing test

This diff only changes two regexes in the D1 SQL splitter, and the tests that cover it pass on all three platforms — on the Windows run, d1/execute.test.ts (16) and d1/export.test.ts (12) are green, and splitter.test.ts passes at 21/21 locally.

Worth noting when comparing against other PRs that appear fully green: they do not run the same set. Because this PR touches wrangler, Turbo invalidates nearly every fixture (0 cached, 94 total here), whereas e.g. #15227 never executed @fixture/interactive-dev at all. The dev-registry flakiness also looks known — there are several active branches on the repo (exp/dev-registry-crash-bisect, fix/dev-registry-teardown-order, fix/defer-dev-registry-registration) investigating it.

Happy to rebase or push an empty commit for a re-run if that helps.

MatheusMartinho and others added 3 commits August 17, 2026 11:46
The statement scanner handled ', " and backtick quoting but not SQLite's
bracket-quoted identifiers, while normalizeSqlLineEndings() in the same
file already did. A ; inside such an identifier, as in
CREATE TABLE metrics ([value;unit] TEXT), was treated as a statement
boundary and broken fragments were sent to D1 (pre-existing, cloudflare#15228).
With the punctuation-delimited markers from this branch the gap also
made a [end] column inside a trigger body pop the compound statement
early and chop the trigger apart.

Consume [ ... ] like the other quote styles, and cover both cases in
the tests.
The ASCII-only class treated a letter with a diacritical mark as a
delimiter, so a column such as néend matched the END marker and closed
a trigger body early. Match identifier characters with \p{L}\p{N}
instead, consistent with how isDollarQuoteIdentifier() in the same file
treats letters with diacritics.
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/d1
  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/brave-brackets-split.md: [@cloudflare/wrangler]
  • .changeset/olive-donuts-battle.md: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/d1/splitter.test.ts: [@cloudflare/d1 @cloudflare/wrangler]
  • packages/wrangler/src/d1/splitter.ts: [@cloudflare/d1 @cloudflare/wrangler]

@MatheusMartinho

Copy link
Copy Markdown
Author

Now rebased on main (0fde4b0), and updating my earlier note on the red checks, because the failing set has changed since I wrote it.

All three fixtures jobs are green now. The two red ones are both in packages-and-tools, and neither touches D1:

Job Failing test Symptom
Tests (Linux, packages-and-tools) @cloudflare/vitest-plugin : test/new-config.test.ts > evaluates config functions with the Vite mode Error: Test timed out in 15000ms
Tests (Windows, packages-and-tools) miniflare : test/dev-registry.spec.ts > DevRegistry > registers after a conflicting entry becomes stale AssertionError: expected 'previous-instance' to be 'f6c6c4ac-54dc-428c-abde-df4488f6d098'

This diff only changes the D1 SQL splitter in packages/wrangler/src/d1/splitter.ts, and the tests that cover it are green on all three platforms: splitter.test.ts passes 21/21, and on the Windows run d1/execute.test.ts and d1/export.test.ts are both green.

The dev-registry failure looks like the flake already being tracked on exp/dev-registry-crash-bisect, fix/dev-registry-teardown-order and fix/defer-dev-registry-registration.

Both Devin findings are fixed and I have resolved those two threads: bracket-quoted identifiers are now consumed by splitSqlIntoStatements() (de80155) and the marker regexes use Unicode identifier classes (d543f3a), with regression tests for the [end], [value;unit] and néend cases.

Ready for a codeowner from @cloudflare/d1 or @cloudflare/wrangler whenever there is a moment. Happy to push an empty commit for a CI re-run if that helps.

@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/d1
  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/brave-brackets-split.md: [@cloudflare/wrangler]
  • .changeset/olive-donuts-battle.md: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/d1/splitter.test.ts: [@cloudflare/d1 @cloudflare/wrangler]
  • packages/wrangler/src/d1/splitter.ts: [@cloudflare/d1 @cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

* include letters with diacritical marks, as in `isDollarQuoteIdentifier()`,
* so a `néend` column does not match either.
*/
const COMPOUND_STATEMENT_START = /(?<![\p{L}\p{N}_$])(BEGIN|CASE)\s$/iu;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Parenthesized CASE breaks trigger splitting

When a trigger contains CASE(, COMPOUND_STATEMENT_START misses the case expression. Its END closes the trigger early, splitting remaining statements apart.

Prompt for agents
Update the compound-statement start matching in packages/wrangler/src/d1/splitter.ts so BEGIN and CASE are recognized when followed by any valid non-identifier delimiter, not only whitespace. Preserve the guard against keyword suffixes in identifiers. Add a splitter regression test with a trigger containing CASE(expression) and at least one subsequent statement inside the trigger, followed by another top-level statement, so premature closure is observable.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

* so a `néend` column does not match either.
*/
const COMPOUND_STATEMENT_START = /(?<![\p{L}\p{N}_$])(BEGIN|CASE)\s$/iu;
const COMPOUND_STATEMENT_END = /(?<![\p{L}\p{N}_$])END[^\p{L}\p{N}_$]$/iu;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Decomposed identifiers break trigger splitting

With a combining accent before end, COMPOUND_STATEMENT_END mistakes part of an identifier for the terminator. Remaining trigger statements become invalid commands.

Prompt for agents
Include Unicode combining marks in the identifier character class used by both compound marker regexes in packages/wrangler/src/d1/splitter.ts. Add a regression test using an unquoted decomposed Unicode identifier whose suffix is `end` inside a multi-statement trigger, then verify the trigger and a following top-level statement remain correctly split.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

[wrangler] D1 SQL splitter does not treat bracket-quoted identifiers as quoted

2 participants